/**
 * Copyright (c) 2012 Nokia Corporation. 
 */

package com.nokia.example.tictactoe.ui;

import java.io.IOException;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

import com.nokia.example.tictactoe.engine.GameModel;
import com.nokia.example.tictactoe.engine.AbstractGameManager;
import com.nokia.example.tictactoe.engine.IGameModelListener;
import com.nokia.mid.ui.CanvasGraphicsItem;

/**
 * Visual representation of the game grid.
 */
public class GameGrid
	extends CanvasGraphicsItem
	implements IGameModelListener, ITouchListener
{
	// Constants
	public static final int BOARD_SIZE = 240;
	private static final int BOARD_MARGIN = 8;
	private static final int LIGHT_LINE_COLOR = 0xff6b6d70;
	private static final int DARK_LINE_COLOR = 0xff595a5b;
	private static final int MARK_SIZE_TO_SQUARE_RATIO = 100; // Percents
	
	// Members
	private GameModel _gameModel = null;
	private AbstractGameManager _gameManager = null;
	private Image _boardImage = null;
	private Image _markXImage = null;
	private Image _markOImage = null;
	private Image _markXWinningImage = null;
	private Image _markOWinningImage = null;
	private int _squareSize = 0;
	private int[] _lastPressedIndex;
	private int[] _winningMarks;
	
	/** 
	 * @param model The game model.
	 * @return A newly created GameGrid instance.
	 */
	static public GameGrid newDefaultSizeGrid(GameModel model) {
		return new GameGrid(BOARD_SIZE, BOARD_SIZE, model);
	}
	
	/**
	 * Constructor.
	 * @param width
	 * @param height
	 * @param model
	 */
	public GameGrid(int width, int height, GameModel model) {
		super(width, height);
		
		if (model == null) {
			throw new IllegalArgumentException();
		}

		try {
			_boardImage = Image.createImage("/graphics/board.png");	
		}
		catch (IOException e) {	}
		
		_gameModel = model;
		_gameModel.addListener(this);
		onGameSpecificationsChanged(); // Initialize the values
		
		_lastPressedIndex = new int[2];
		_lastPressedIndex[0] = -1;
		_lastPressedIndex[1] = -1;
	}

	/**
	 * From CanvasGraphicsItem.
	 */
	protected void paint(Graphics gc) {
		// Draw the board
		if (_boardImage != null) {
			gc.drawImage(_boardImage, 0, 0, Graphics.TOP | Graphics.LEFT);
		}
		
		// Draw the lines
		final int gridSizeSquared = _gameModel.gridSizeSquared();
		int tempX;
		int tempY;
		
		// Horizontal lines
		for (int i = 0; i < gridSizeSquared - 1; ++i) {
			tempY = BOARD_MARGIN + _squareSize * (i + 1);
			gc.setColor(DARK_LINE_COLOR);
			gc.drawLine(BOARD_MARGIN, tempY - 1, BOARD_SIZE - BOARD_MARGIN, tempY - 1);
			gc.setColor(LIGHT_LINE_COLOR);
			gc.drawLine(BOARD_MARGIN, tempY, BOARD_SIZE - BOARD_MARGIN, tempY);
			gc.drawLine(BOARD_MARGIN, tempY + 1, BOARD_SIZE - BOARD_MARGIN, tempY + 1);
		}
		
		// Vertical lines
		for (int i = 0; i < gridSizeSquared - 1; ++i) {
			tempX = BOARD_MARGIN + _squareSize * (i + 1);
			gc.setColor(LIGHT_LINE_COLOR);
			gc.drawLine(tempX - 1, BOARD_MARGIN,  tempX - 1, BOARD_SIZE - BOARD_MARGIN);
			gc.drawLine(tempX, BOARD_MARGIN,  tempX, BOARD_SIZE - BOARD_MARGIN);
			gc.setColor(DARK_LINE_COLOR);			
			gc.drawLine(tempX + 1, BOARD_MARGIN,  tempX + 1, BOARD_SIZE - BOARD_MARGIN);
		}		
		
		// Draw the marks
		int mark;
		final int winner = _gameModel.winner();
		boolean winningMark = false;
		
		for (int y = 0; y < gridSizeSquared; ++y) {
			for (int x = 0; x < gridSizeSquared; ++x) {
				mark = _gameModel.markAt(x, y);
				
				if (mark == GameModel.MARK_EMPTY) {
					// Nothing to draw
					continue;
				}
				
				if (winner == GameModel.MARK_X || winner == GameModel.MARK_O) {
					// We have a winner
					for (int i = 0; i < _winningMarks.length; i += 2) {
						if (x == _winningMarks[i] && y == _winningMarks[i + 1]) {
							winningMark = true;
							break;
						}
					}					
				}

				tempX = _squareSize * x + BOARD_MARGIN;
				tempY = _squareSize * y + BOARD_MARGIN;
				
				if (mark == GameModel.MARK_X) {
					if (winningMark) {
						gc.drawImage(_markXWinningImage, tempX, tempY,
									 Graphics.TOP | Graphics.LEFT);
					}
					else {
						gc.drawImage(_markXImage, tempX, tempY,
									 Graphics.TOP | Graphics.LEFT);
					}
				}
				else if (mark == GameModel.MARK_O) {
					if (winningMark) {
						gc.drawImage(_markOWinningImage, tempX, tempY,
									 Graphics.TOP | Graphics.LEFT);
					}
					else {
						gc.drawImage(_markOImage, tempX, tempY,
								     Graphics.TOP | Graphics.LEFT);
					}
				}
				
				winningMark = false;
			}
		}
	}
	
	/**
	 * From GameModelListener.
	 * Requests a repaint since the game model has been changed.
	 */
	public void onGameModelChanged() {
		repaint();
	}
	
	/**
	 * From GameModelListener.
	 */	
	public void onWhoseTurnChanged(int whoseTurn) {}	
	
	/**
	 * From GameModelListener.
	 */
	public void onGameOver(int winner) {
		if (winner == GameModel.MARK_X) {
			_winningMarks = _gameModel.winningMarks();
		}
		else if (winner == GameModel.MARK_O) {
			_winningMarks = _gameModel.winningMarks();
		}
		else {
			// It's a tie
		}
		
		if (_gameManager != null) {
			_gameManager.onGameOver();
		}
	}
	
	/**
	 * From GameModelListener.
	 */
	public void onGameSpecificationsChanged() {
		_squareSize = (BOARD_SIZE - BOARD_MARGIN * 2) / _gameModel.gridSizeSquared();
		initMarkImages();
		System.out.println("GameGrid::onGameSpecificationsChanged(): Square size is "
				+ _squareSize);
		repaint();
	}
	
	/**
	 * 
	 * @param manager
	 */
	public void setGameManager(AbstractGameManager manager) {
		_gameManager = manager;
	}
	
	/**
	 * Stores the pressed index for release event.
	 * @param x X coordinate of the pressed event.
	 * @param y Y coordinate of the pressed event.
	 */
	public void onPressed(int x, int y) {
		if (x < 0 || x > BOARD_SIZE || y < 0 || y > BOARD_SIZE) {
			return;
		}
		
		if (_gameModel.winner() == -1) {
			_lastPressedIndex = pressedIndex(x, y);
		}
		else {
			// The current game is over, start a new game
			_lastPressedIndex[0] = -1;
			_lastPressedIndex[1] = -1;			
			
			if (_gameManager != null) {
				_gameManager.startGame();
			}
		}
	}
	
	/**
	 * Checks if the release event happened on the same grid item as the
	 * pressed event. If so, inserts the mark of the player whose turn it is to
	 * that grid position.
	 * @param x X coordinate of the release event.
	 * @param y Y coordinate of the release event.
	 */
	public void onReleased(int x, int y) {
		final int[] releasedIndex = pressedIndex(x, y);
		
		if (_lastPressedIndex[0] != -1
				&& _lastPressedIndex[0] == releasedIndex[0]
				&& _lastPressedIndex[1] == releasedIndex[1])
		{
			_gameManager.onSquarePressed(releasedIndex[0], releasedIndex[1]);
		}
		
		_lastPressedIndex[0] = -1;
		_lastPressedIndex[1] = -1;
	}
	
	/**
	 * Resolves the pressed grid item and returns its coordinates.
	 * @param x X coordinate of the pressed event.
	 * @param y Y coordinate of the pressed event.
	 * @return The coordinates of the pressed grid item.
	 */
	private final int[] pressedIndex(int x, int y) {
		int[] retval = new int[2];
		x -= BOARD_MARGIN;
		y -= BOARD_MARGIN;		
		retval[0] = x / _squareSize;
		retval[1] = y / _squareSize;
		return retval;
	}
	
	/**
	 * 
	 */
	private void initMarkImages() {
		_markXImage = null;
		_markOImage = null;
		_markXWinningImage = null;
		_markOWinningImage = null;
		
		Image[] originalMarkImages = new Image[4];
		
		try {	
			originalMarkImages[0] = Image.createImage("/graphics/X.png");
			originalMarkImages[1] = Image.createImage("/graphics/O.png");
			originalMarkImages[2] = Image.createImage("/graphics/X-won.png");
			originalMarkImages[3] = Image.createImage("/graphics/O-won.png");
		}
		catch (IOException e) {	}
		
		if (_gameModel.gridSizeSquared() == 3) {
			// No need to scale
			_markXImage = originalMarkImages[0];
			_markOImage = originalMarkImages[1];
			_markXWinningImage = originalMarkImages[2];
			_markOWinningImage = originalMarkImages[3];
			return;
		}
		
		int scaleToSize = _squareSize * MARK_SIZE_TO_SQUARE_RATIO / 100;
		System.out.println("GameGrid::initMarkImages(): New mark image size is "
				+ scaleToSize);
		
		Image[] scaledMarkImages = new Image[4];
		
		for (int i = 0; i < 4; ++i) {
			scaledMarkImages[i] =
					pixelMixing(originalMarkImages[i], scaleToSize, scaleToSize);
		}
	
		_markXImage = scaledMarkImages[0];
		_markOImage = scaledMarkImages[1];
		_markXWinningImage = scaledMarkImages[2];
		_markOWinningImage = scaledMarkImages[3];
	}
    
    /**
     * Scales image by mixing pixels to the scaled image. Works best with down scaling.
     * @return Returns scaled image
     */
    private static Image pixelMixing(Image original,
                                     int newWidth,
                                     int newHeight)
    {
        int[] rawInput = new int[original.getHeight() * original.getWidth()];
        original.getRGB(rawInput, 0, original.getWidth(), 0, 0,
                        original.getWidth(), original.getHeight());

        int[] rawOutput = new int[newWidth * newHeight];

        int oWidth = original.getWidth();
        int[] oX16 = new int[newWidth + 1];
        
        for (int newX = 0; newX <= newWidth; newX++) {
            oX16[newX] = ((newX * oWidth) << 4) / newWidth;
        }

        int[] oXStartWidth = new int[newWidth];
        int[] oXEndWidth = new int[newWidth];
        
        for (int newX = 0; newX < newWidth; newX++) {
            oXStartWidth[newX] = 16 - (oX16[newX] % 16);
            oXEndWidth[newX] = oX16[newX + 1] % 16;
        }

        int oHeight = original.getHeight();
        int[] oY16 = new int[newHeight + 1];
        
        for (int newY = 0; newY <= newHeight; newY++) {
            oY16[newY] = ((newY * oHeight) << 4) / newHeight;
        }

        int oX16Start, oX16End, oY16Start, oY16End;
        int oYStartHeight, oYEndHeight;
        int oXStart, oXEnd, oYStart, oYEnd;
        int outArea, outColorArea, outAlpha, outRed, outGreen, outBlue;
        int areaHeight, areaWidth, area;
        int argb, a, r, g, b;
        
        for (int newY = 0; newY < newHeight; newY++) {
            oY16Start = oY16[newY];
            oY16End = oY16[newY + 1];
            oYStart = oY16Start >>> 4;
            oYEnd = oY16End >>> 4;
            oYStartHeight = 16 - (oY16Start % 16);
            oYEndHeight = oY16End % 16;
            
            for (int newX = 0; newX < newWidth; newX++) {
                oX16Start = oX16[newX];
                oX16End = oX16[newX + 1];
                oXStart = oX16Start >>> 4;
                oXEnd = oX16End >>> 4;
                outArea = 0;
                outColorArea = 0;
                outAlpha = 0;
                outRed = 0;
                outGreen = 0;
                outBlue = 0;
                
                for (int j = oYStart; j <= oYEnd; j++) {
                    areaHeight = 16;
                    
                    if (oYStart == oYEnd) {
                        areaHeight = oY16End - oY16Start;
                    }
                    else if (j == oYStart) {
                        areaHeight = oYStartHeight;
                    }
                    else if (j == oYEnd) {
                        areaHeight = oYEndHeight;
                    }
                    if (areaHeight == 0) {
                        continue;
                    }
                    
                    for (int i = oXStart; i <= oXEnd; i++) {
                        areaWidth = 16;
                        
                        if (oXStart == oXEnd) {
                            areaWidth = oX16End - oX16Start;
                        }
                        else if (i == oXStart) {
                            areaWidth = oXStartWidth[newX];
                        }
                        else if (i == oXEnd) {
                            areaWidth = oXEndWidth[newX];
                        }
                        if (areaWidth == 0) {
                            continue;
                        }

                        area = areaWidth * areaHeight;
                        outArea += area;
                        argb = rawInput[i + j * original.getWidth()];
                        a = (argb >>> 24);
                        
                        if (a == 0) {
                            continue;
                        }
                        
                        area = a * area;
                        outColorArea += area;
                        r = (argb & 0x00ff0000) >>> 16;
                        g = (argb & 0x0000ff00) >>> 8;
                        b = argb & 0x000000ff;
                        outRed += area * r;
                        outGreen += area * g;
                        outBlue += area * b;
                    }
                }
                
                if (outColorArea > 0) {
                    outAlpha = outColorArea / outArea;
                    outRed = outRed / outColorArea;
                    outGreen = outGreen / outColorArea;
                    outBlue = outBlue / outColorArea;
                }
                
                rawOutput[newX + newY * newWidth] = (outAlpha << 24)
                        | (outRed << 16) | (outGreen << 8) | outBlue;
            }
        }
        
        return Image.createRGBImage(rawOutput, newWidth, newHeight, true);
    }    
}
